package zhongbo.javaweb.instagram.service.runner;

import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.Charsets;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.util.StringUtils;
import zhongbo.javaweb.instagram.enums.InsKey;
import zhongbo.javaweb.instagram.schedule.ProxyCheckSchedule;
import zhongbo.javaweb.instagram.util.HttpClientUtil;
import zhongbo.javaweb.instagram.util.sha256.HmacSha256Util;
import zhongbo.javaweb.instagram.util.thread.ProgressProcessor;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 说明：
 * <p>
 * ins下载执行器
 *
 * @author zhongboWork
 * @version 1.0
 * @since 2021-12-20 15:21
 */
@Slf4j
@Data
public class InstagramProcessor implements ProgressProcessor<Document> {

    private MongoTemplate mongoTemplate;
    private MongoCollection<Document> userProfileCollection;
    private MongoCollection<Document> userPostCollection;
    private ProxyCheckSchedule proxyCheckSchedule;
    private String cookie;

    private String botGoogleUserAgent = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)";
    private String botIphoneUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 ";
    private String botChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36";
    private long sleepTime = 1000;

    private int first = 40;
    private int timeOutMs = 10000;
    private String urlPattern = "https://www.instagram.com/graphql/query/?query_id=17888483320059182&id=%s&first=%s";

    public InstagramProcessor(MongoTemplate mongoTemplate, MongoCollection<Document> userProfileCollection, MongoCollection<Document> userPostCollection, ProxyCheckSchedule proxyCheckSchedule) {
        this.mongoTemplate = mongoTemplate;
        this.userProfileCollection = userProfileCollection;
        this.userPostCollection = userPostCollection;
        this.proxyCheckSchedule = proxyCheckSchedule;
    }

    /**
     * @param target 处理目标(userProfile _id, id)
     */
    @Override
    public void process(Document target, int index) {
        if (target != null) {
            String userProfileId = target.getString("id");
            String url = buildUrl(userProfileId);
            if (log.isDebugEnabled()) {
                log.debug("开始下载 {}", url);
            }
            String proxyUrl = peekProxyUrl();
            RequestConfig requestConfig;
            if (StringUtils.isEmpty(proxyUrl)) {
                requestConfig = RequestConfig.custom()
                        .setConnectTimeout(timeOutMs)
                        .setRedirectsEnabled(false)
                        .build();
                if (log.isDebugEnabled()) {
                    log.debug("未使用代理");
                }
            } else {
                HttpHost proxy = HttpHost.create(proxyUrl);
                requestConfig = RequestConfig.custom()
                        .setProxy(proxy)
                        .setConnectTimeout(timeOutMs)
                        .setRedirectsEnabled(false)
                        .build();
                if (log.isDebugEnabled()) {
                    log.debug("使用代理: {}", proxyUrl);
                }
            }
            HttpGet httpGet = new HttpGet(url);
            httpGet.setConfig(requestConfig);
            httpGet.setHeader(HTTP.USER_AGENT, botChromeUserAgent);
            httpGet.setHeader("Cookie", cookie);
            CloseableHttpClient client = HttpClientUtil.createInsClient();
            if (client == null) {
                throw new RuntimeException("error createInsClient null!");
            }
            CloseableHttpResponse execute;
            String body = null;
            try {
                TimeUnit.MILLISECONDS.sleep(sleepTime);
                execute = client.execute(httpGet);
                int statusCode = execute.getStatusLine().getStatusCode();
                int statusOk = 200;
                if (statusCode == statusOk) {
                    body = EntityUtils.toString(execute.getEntity(), Charsets.UTF_8);
                } else {
                    checkErrors(execute);
                }
            } catch (Exception e) {
                String msg = String.format("抓取异常! error:%s", e.getMessage());
                throw new RuntimeException(msg, e);
            }
            insertBody(body);
        } else {
            log.warn("用户信息为null, 无法处理此数据, index: {}", index);
        }
    }

    private void insertBody(String body) {
        if (StringUtils.isEmpty(body)) {
            String msg = String.format("存入数据失败! body: %s error: 抓取数据为空", body);
            throw new RuntimeException(msg);
        }
        try {
            Document doc = Document.parse(body);
            if (InsKey.OK.equals(doc.get(InsKey.STATUS))) {
                Document docData = (Document) doc.get(InsKey.DATA);
                Document docUser = (Document) docData.get(InsKey.USER);
                Document docEdgeOwner = (Document) docUser.get(InsKey.EDGE_OWNER_TO_TIMELINE_MEDIA);
                int count = docEdgeOwner.getInteger(InsKey.COUNT, 0);
                List<Document> edges = (List<Document>) docEdgeOwner.get(InsKey.EDGES);
                for (Document edge : edges) {
                    Document node = parseNode(edge);
                    addOrUpdateNode(node);
                }
            }
        } catch (Exception e) {
            String msg = String.format("userPost 存入数据库失败! body: %s error: %s", body, e.getMessage());
            throw new RuntimeException(msg, e);
        }
    }

    private void addOrUpdateNode(Document node) {
        if (node != null) {
            String postId = node.getString(InsKey.ID);
            Bson bson = Filters.eq("id", postId);
            FindIterable<Document> documents = userPostCollection.find(bson);
            Document first = documents.first();
            if (first != null) {
                List<Document> timingSequenceData = (List<Document>) node.get(InsKey.TIMING_SEQUENCE_DATA);
                if (timingSequenceData != null && !timingSequenceData.isEmpty()) {
                    Document docUpdate = new Document();
                    docUpdate.put("$addToSet", timingSequenceData.get(0));
                    userPostCollection.findOneAndUpdate(bson, docUpdate);
                }
            } else {
                userPostCollection.insertOne(node);
            }
        }
    }

    private Document parseNode(Document edge) {
        Document node = null;
        try {
            node = (Document) edge.get(InsKey.NODE);
            node = Document.parse(node.toJson());
            String postId = node.getString(InsKey.ID);
            if (StringUtils.isEmpty(postId)) {
                log.error("未获取帖子id! postId: {}", postId);
                return null;
            }
            Document owner = (Document) node.get(InsKey.OWNER);
            String userId = owner.getString(InsKey.ID);
            if (StringUtils.isEmpty(userId)) {
                log.error("未获取用户id! postId: {} json: {}", postId, node.toJson());
                return null;
            }
            List<String> imageUrls = parseImages(node);
            List<String> imagePath = downLoadImages(imageUrls, userId);
            List<Document> timingSequenceData = parseTimingSequenceData(node);
            node.put(InsKey.IMAGE_PATHS, imagePath);
            node.put(InsKey.TIMING_SEQUENCE_DATA, timingSequenceData);
        } catch (Exception e) {
            log.error("解析节点失败! parseNode error:", e);
        }
        return node;
    }

    private List<Document> parseTimingSequenceData(Document node) {
        List<Document> edgeMedia = new ArrayList<>();
        Document docLike = (Document) node.get(InsKey.EDGE_MEDIA_PREVIEW_LIKE);
        int docEdgeMediaInteger = 0;
        if (docLike != null) {
            docEdgeMediaInteger = docLike.getInteger(InsKey.COUNT, 0);
        }
        Document docComment = (Document) node.get(InsKey.EDGE_MEDIA_TO_COMMENT);
        int docCommentInteger = 0;
        if (docComment != null) {
            docCommentInteger = docComment.getInteger(InsKey.COUNT, 0);
        }
        Document mediaNode = new Document();
        mediaNode.put(InsKey.LIKE_COUNT, docEdgeMediaInteger);
        mediaNode.put(InsKey.COMMENT_COUNT, docCommentInteger);
        mediaNode.put(InsKey.CURRENT_TIME, new Date());
        edgeMedia.add(mediaNode);
        return edgeMedia;
    }

    private List<String> downLoadImages(List<String> imageUrls, String ownerId) {
        List<String> imagePath = new ArrayList<>();
        for (String imageUrl : imageUrls) {
            CloseableHttpClient httpClient = HttpClientUtil.chooseInsClient();
            try {
                HttpUriRequest httpGet = new HttpGet(imageUrl);
                httpGet.addHeader(HTTP.USER_AGENT, botChromeUserAgent);
                httpClient.execute(httpGet);
                byte[] bytes = HttpClientUtil.doGet(imageUrl);
                if (bytes != null) {
                    String sha256 = HmacSha256Util.getHmacSha256(bytes);
                    if (StringUtils.isEmpty(sha256)) {
                        sha256 = UUID.randomUUID().toString().replace("-", "");
                    }
                    String filePath = InsKey.IMAGES_STORE + File.separator + ownerId + File.separator + sha256 + ".jpg";
                    File file = new File(filePath);
                    if (file.exists()) {
                        log.warn("图片已存在, 跳过保存图片, filePath: {}, imageUrl: {}", filePath, imageUrl);
                        continue;
                    }
                    FileOutputStream fileOutputStream = new FileOutputStream(file);
                    fileOutputStream.write(bytes);
                    fileOutputStream.flush();
                    fileOutputStream.close();
                    imagePath.add(filePath);
                }
            } catch (Exception e) {
                log.error("下载图片失败! imageUrl: {}", imageUrl, e);
            }
        }
        return imagePath;
    }

    private List<String> parseImages(Document node) {
        List<String> imageUrls = new ArrayList<>();
        String parseImagesFrom = "";
        if (node != null) {
            String displayUrl = node.getString(InsKey.DISPLAY_URL);
            if (!StringUtils.isEmpty(displayUrl)) {
                parseImagesFrom = InsKey.DISPLAY_URL;
                imageUrls.add(displayUrl);
            } else {
                try {
                    Object displayResources = node.get(InsKey.DISPLAY_RESOURCES);
                    if (displayResources instanceof List) {
                        List<?> list = (List<?>) displayResources;
                        for (Object o : list) {
                            if (o instanceof Document) {
                                Document doc = (Document) o;
                                String src = doc.getString(InsKey.THUMBNAIL_SRC);
                                if (!StringUtils.isEmpty(src)) {
                                    imageUrls.add(src);
                                    parseImagesFrom = InsKey.DISPLAY_RESOURCES;
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    log.error("获取图片失败:1 {}! {}", InsKey.DISPLAY_RESOURCES, node.toJson(), e);
                }
            }
            if (imageUrls.isEmpty()) {
                try {
                    String thumbnailSrc = node.getString(InsKey.THUMBNAIL_SRC);
                    if (!StringUtils.isEmpty(thumbnailSrc)) {
                        imageUrls.add(thumbnailSrc);
                        parseImagesFrom = InsKey.THUMBNAIL_SRC;
                    }
                } catch (Exception e) {
                    log.error("获取图片失败:2 {}! {}", InsKey.THUMBNAIL_SRC, node.toJson(), e);
                }
            }
            if (imageUrls.isEmpty()) {
                Object thumbnailResources = node.get(InsKey.THUMBNAIL_RESOURCES);
                try {
                    if (thumbnailResources instanceof List) {
                        List<?> list = (List<?>) thumbnailResources;
                        for (Object o : list) {
                            if (o instanceof Document) {
                                Document doc = (Document) o;
                                String src = doc.getString(InsKey.SRC);
                                if (!StringUtils.isEmpty(src)) {
                                    imageUrls.add(src);
                                    parseImagesFrom = InsKey.THUMBNAIL_RESOURCES;
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    log.error("获取图片失败:3 {}! {}", InsKey.THUMBNAIL_RESOURCES, node.toJson(), e);
                }
            }
        }
        log.debug("获取图片来源: {}", parseImagesFrom);
        return imageUrls;
    }

    private void checkErrors(CloseableHttpResponse execute) {
        int statusCode = execute.getStatusLine().getStatusCode();
        String locationStr = "Location";
        Header firstHeader = execute.getFirstHeader(locationStr);
        if (firstHeader != null) {
            String value = firstHeader.getValue();
            String msg = String.format("登录cookie过期! cookie=%s status=%s", value, statusCode);
            throw new RuntimeException(msg);
        } else {
            String msg = String.format("登录验证失败, 无法继续! status=%s", statusCode);
            throw new RuntimeException(msg);
        }
    }

    private String peekProxyUrl() {
        String availableProxy = proxyCheckSchedule.getAvailableProxy();
        if (StringUtils.isEmpty(availableProxy)) {
            throw new RuntimeException("执行异常,无可用代理,任务无法执行");
        }
        return availableProxy;
    }

    private String buildUrl(String userProfileId) {
        return String.format(urlPattern, userProfileId, first);
    }

}
